/// API server for pudding, to explore the rust world
use std::time::Duration;

use axum::{
    body::Bytes,
    extract::MatchedPath,
    http::{HeaderMap, Request},
    response::Response,
    Router,
};
use tokio::sync::{mpsc, oneshot};
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
use tracing::{info_span, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod app_state;
mod grpc_client;
mod redis_client;
mod rest_api;
mod types;
mod utils;

// Print all routes
// fn print_routes(router: &Router) {
//     for (path, methods) in router.inner().routes.iter().filter_map(|r| r.into_parts()) {
//         for method in methods {
//             println!("{} {}", method.method(), path);
//         }
//     }
// }

#[tokio::main]
async fn main() {
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
                // axum logs rejections from built-in extractors with the `axum::rejection`
                // target, at `TRACE` level. `axum::rejection=trace` enables showing those events
                "apiserver=debug,tower_http=debug,axum::rejection=trace".into()
            }),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    let (tx_to_rauth_grpc, rx_by_rauth_grpc) = mpsc::channel::<(
        types::channel::Request,
        oneshot::Sender<types::channel::Response>,
    )>(1);

    let (tx_to_iblog_grpc, rx_by_iblog_grpc) = mpsc::channel::<(
        types::channel::Request,
        oneshot::Sender<types::channel::Response>,
    )>(1);

    let (tx_to_redis, rx_by_redis) = mpsc::channel::<(
        types::channel::Request,
        oneshot::Sender<types::channel::Response>,
    )>(1);

    let app = Router::new()
        .fallback(fallback)
        .nest(
            "/api/v1",
            rest_api::routes::v1_new(tx_to_rauth_grpc, tx_to_iblog_grpc, tx_to_redis),
        )
        // .with_state(Arc::new(RwLock::new(AppState::new())))
        // `TraceLayer` is provided by tower-http so you have to add that as a dependency.
        // It provides good defaults but is also very customizable.
        //
        // See https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html for more details.
        //
        // If you want to customize the behavior using closures here is how.
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(|request: &Request<_>| {
                    // Log the matched route's path (with placeholders not filled in).
                    // Use request.uri() or OriginalUri if you want the real path.
                    let matched_path = request
                        .extensions()
                        .get::<MatchedPath>()
                        .map(MatchedPath::as_str);

                    info_span!(
                        "http_request",
                        method = ?request.method(),
                        matched_path,
                        some_other_field = tracing::field::Empty,
                    )
                })
                .on_request(|_request: &Request<_>, _span: &Span| {
                    // You can use `_span.record("some_other_field", value)` in one of these
                    // closures to attach a value to the initially empty field in the info_span
                    // created above.
                    // 在请求开始时记录信息
                    _span.record(
                        "user_agent",
                        &_request
                            .headers()
                            .get("User-Agent")
                            .cloned()
                            .unwrap()
                            .to_str()
                            .unwrap(),
                    );
                    _span.record("uri", &_request.uri().path_and_query().unwrap().as_str());

                    tracing::debug!("Request started: {:?}", &_request);
                })
                .on_response(|_response: &Response, _latency: Duration, _span: &Span| {
                    // 在响应完成时记录信息
                    _span.record("status_code", &_response.status().as_u16());
                    _span.record("latency", &_latency.as_millis());

                    tracing::debug!("Response completed: {:?}", _response);
                })
                .on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
                    // ...
                })
                .on_eos(
                    |_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
                        // ...
                    },
                )
                .on_failure(
                    |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
                        // 在请求失败时记录信息
                        _span.record("error", &_error.to_string());
                        _span.record("latency", &_latency.as_millis());

                        tracing::error!("Request failed: {:?}", _error);
                    },
                ),
        );

    let app_k8s_healthy = Router::new().nest("/api/v1", rest_api::routes::v1_healthy_new());

    // print_routes(&app);
    // print_routes(&app_k8s_healthy);

    let _ = tokio::join!(
        start_server(app, "0.0.0.0:80"),
        start_server(app_k8s_healthy, "0.0.0.0:9000"),
        grpc_client::client::start(rx_by_rauth_grpc, rx_by_iblog_grpc),
        redis_client::client::start(rx_by_redis),
    );
}

async fn start_server(app: Router, addr: &str) {
    // println!("Listening on http://{addr}");
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

/// axum handler for any request that fails with the router routes.
/// This implementation returns HTTP status code Not Found (404).
pub async fn fallback(uri: axum::http::Uri) -> impl axum::response::IntoResponse {
    tracing::debug!("No route {}", uri.to_string());
    (
        axum::http::StatusCode::NOT_FOUND,
        format!("No route {}", uri),
    )
}

/// axum handler for "GET /" which returns a string and causes axum to
/// immediately respond with status code `200 OK` and with the string.
pub async fn hello() -> String {
    "Hello, World!".to_string()
}
